import os
import re
from contextlib import asynccontextmanager
from datetime import datetime, timedelta

from dotenv import load_dotenv
from fastapi import Depends, FastAPI, Path, Request, Response
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from sqlalchemy import delete, insert, select
from sqlalchemy.exc import ProgrammingError
from starlette.staticfiles import StaticFiles

from core import models
from core.database import DBConnect
from core.exception import AlertException, regist_core_exception_handler, template_response
from core.middleware import regist_core_middleware, should_run_middleware
from core.plugin import (
    cache_plugin_menu, cache_plugin_state, get_plugin_state_change_time,
    import_plugin_by_states, read_plugin_state, register_plugin,
    register_plugin_admin_menu, register_statics,
)
from core.routers import router as template_router  #这里模版router 前端的现实
from core.settings import ENV_PATH, settings
from core.template import register_theme_statics
from lib.common import (
    get_client_ip, is_intercept_ip, is_possible_ip, session_member_key
)
from lib.dependency.dependencies import check_use_template
from lib.member import is_super_admin
from lib.scheduler import scheduler
from lib.token import create_session_token
from service.member_service import MemberService
from service.point_service import PointService
from service.visit_service import VisitService

from admin.admin import router as admin_router
from install.router import router as install_router
from bbs.login import router as login_router

from api.v1.routers import router as api_router  #这里纯后端接口；

# .env 它将从文件加载环境变量.
# 此函数是该文件中的一个键-它用于将值对加载到环境变量中.
load_dotenv()


@asynccontextmanager
async def lifespan(app: FastAPI):
    """
    定义在应用开始和结束时运行的代码。
    -yield之前的代码：在服务器启动时运行
    -yield之后的代码：在服务器关闭时运行
    """
    yield
    scheduler.remove_flag()

app = FastAPI(
    debug=settings.APP_IS_DEBUG,  # 调试模式设置为激活 #启用调试模式
    lifespan=lifespan,
    title="nouboard6",
    description=""
)
#如果从git clone收到源，则必须创建它，因为没有数据目录

# git clone如果您收到的来源为 data没有目录，必须创建
if not os.path.exists("data"):
    os.mkdir("data")
    
#将每个路径中的文件注册为静态文件。
# 将每个路径中的文件注册为固定文件.
register_theme_statics(app)
app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/data", StaticFiles(directory="data"), name="data")
 
# 插件路由器优先级注册 #先注册插件路由器

plugin_states = read_plugin_state()
import_plugin_by_states(plugin_states)
register_plugin(plugin_states)
register_statics(app, plugin_states)

cache_plugin_state.__setitem__('info', plugin_states)
cache_plugin_state.__setitem__('change_time', get_plugin_state_change_time())
cache_plugin_menu.__setitem__('admin_menus', register_plugin_admin_menu(plugin_states))

app.include_router(admin_router)
app.include_router(api_router)
app.include_router(template_router)
app.include_router(install_router)
app.include_router(login_router)


@app.middleware("http")
async def main_middleware(request: Request, call_next):
    """始终根据请求运行"""
# “每次请求都运行的中间件”
    if not await should_run_middleware(request):
        return await call_next(request)

    # 检查是否安装数据库
    with DBConnect().sessionLocal() as db:
        url_path = request.url.path
        config = None

        try:
            if not url_path.startswith("/install"):
                if not os.path.exists(ENV_PATH):
                    raise AlertException(".env 没有文件. 请继续安装.", 400, "/install")#env文件不存在。请进行安装。", 400, "/install"
                # 基础环境设置表查询  #查看首选参数表
                config = db.scalar(select(models.Config))
            else:
                return await call_next(request)

        except AlertException as e:
            context = {"request": request, "errors": e.detail, "url": e.url}
            return template_response("alert.html", context, e.status_code)

        except ProgrammingError as e:
            context = {
                "request": request,
                "errors": "DB 不存在表或设置信息. 请再次继续安装."#,数据库表或设置信息不存在。请重新进行安装。",

                "url": "/install"
            }
            return template_response("alert.html", context, 400)

        # 点亮并设置默认环境设置   #查看和设置默认首选参数
        request.state.config = config
        request.state.title = config.cf_title

        # 整个设计器变量 #编辑器全局变量
        request.state.editor = config.cf_editor
        request.state.use_editor = True if config.cf_editor else False

        # Cookie域-所有变量  #Cookie域全局变量
        request.state.cookie_domain = cookie_domain = settings.COOKIE_DOMAIN

        member = None
        is_autologin = False
        ss_mb_key = None
        session_mb_id = request.session.get("ss_mb_id", "")
        cookie_mb_id = request.cookies.get("ck_mb_id", "")
        current_ip = get_client_ip(request)

        try:
            member_service = MemberService(request, db)
            # 如果您正在维护登录会话 #如果您正在保留登录会话
            if session_mb_id:
                member = member_service.get_member(session_mb_id)
                # 如果您没有成员资格信息或您是已离开的成员，请初始化会话 #如果您没有会员信息或已退出，请初始化会话
                if not member_service.is_activated(member)[0]:
                    request.session.clear()
                    member = None

            # 如果您有自动登录cookie  #如果有自动登录Cookie
            elif cookie_mb_id:
                mb_id = re.sub("[^a-zA-Z0-9_]", "", cookie_mb_id)[:20]
                member = member_service.get_member(session_mb_id)

                # 高层管理人员不使用自动登录功能来确保安全.
                if (not is_super_admin(request, mb_id)
                        and member_service.is_member_email_certified(member)[0]
                        and member_service.is_activated(member)[0]):
                    # 检查cookie中存储的密钥与服务器生成的密钥是否匹配
                    ss_mb_key = session_member_key(request, member)
                    if request.cookies.get("ck_auto") == ss_mb_key:
                        request.session["ss_mb_id"] = cookie_mb_id
                        is_autologin = True
        except AlertException as e:
            context = {"request": request, "errors": e.detail, "url": "/"}
            response = template_response("alert.html", context, e.status_code)
            response.delete_cookie("ck_auto")
            response.delete_cookie("ck_mb_id")
            request.session.clear()
            return response

        if member:
            # 如果您是今天的第一次，请更新您的点付款和登录信息 #如果是今天第一次登录，请支付积分并更新登录信息
            ymd_str = datetime.now().strftime("%Y-%m-%d")
            if member.mb_today_login.strftime("%Y-%m-%d") != ymd_str:
                point_service = PointService(request, db, member_service)
                point_service.save_point(
                    member.mb_id, config.cf_login_point, ymd_str + " 首次登录",
                    "@login", member.mb_id, ymd_str)

                member.mb_today_login = datetime.now()
                member.mb_login_ip = request.client.host
                db.commit()

        # 登录的会员信息 #登录的会员信息
        request.state.login_member = member
        # 是否为最高管理者
        request.state.is_super_admin = is_super_admin(request, getattr(member, "mb_id", None))

        # 可访问的/阻塞 IP 检查
        # - IP 使用check函数时 is_super_admin 在登录代码之后运行，因为它检查是否
        if not is_possible_ip(request, current_ip):
            return HTMLResponse("<meta charset=utf-8>不允许访问 IP 就这样..")
        if is_intercept_ip(request, current_ip):
            return HTMLResponse("<meta charset=utf-8>访问被阻止 IP 就这样..")

    # 设置响应对象
    response: Response = await call_next(request)

    with DBConnect().sessionLocal() as db:
        age_1day = 60 * 60 * 24
#重置自动登录Cookie
# 检查与#is_autologn的会话，防止在注销处理后重置Cookie
        if is_autologin and request.session.get("ss_mb_id"):
            response.set_cookie(key="ck_mb_id", value=cookie_mb_id,
                                max_age=age_1day * 30, domain=cookie_domain)
            response.set_cookie(key="ck_auto", value=ss_mb_key,
                                max_age=age_1day * 30, domain=cookie_domain)
        # 记录访客历史记录
        ck_visit_ip = request.cookies.get('ck_visit_ip', None)
        if ck_visit_ip != current_ip:
            response.set_cookie(key="ck_visit_ip", value=current_ip,
                                max_age=age_1day, domain=cookie_domain)
            visit_service = VisitService(request, db)
            visit_service.create_visit_record()

    return response

#添加要默认运行的中间件的函数
#函数必须位于main_middleware函数的下面。
#否则，您可能会遇到以下错误：
# AssertionError: SessionMiddleware must be installed to access 
regist_core_middleware(app)

#注册默认异常处理处理程序的函数
regist_core_exception_handler(app)

#注册并运行调度程序
scheduler.run_scheduler()


@app.post("/generate_token",
          include_in_schema=False)
async def generate_token(request: Request) -> JSONResponse:
    """创建会话令牌后返回

    Args:
        request (Request): FastAPI的 Request 对象

    Returns:
        JSONResponse: 成功和包括令牌 JSON 响应
    """
    token = create_session_token(request)

    return JSONResponse(content={"success": True, "token": token})


@app.get("/device/change/{device}",
         dependencies=[Depends(check_use_template)],
         include_in_schema=False)
async def device_change(
    request: Request,
    device: str = Path(...)
) -> RedirectResponse:

    """更改连接环境（设备）
    -强制更改PC/移动版本。

    Args:
    request：FastAPI中的request对象
    device（str，optional）：要更改的设备。Defaults to Path(...).

    Returns:
    RedirectResponse:重定向到上一页
    """
    if (device in ["pc", "mobile"]
            and not settings.IS_RESPONSIVE):
        if device == "pc":
            request.session["is_mobile"] = False
        else:
            request.session["is_mobile"] = True

    referer = request.headers.get("Referer", "/")
    return RedirectResponse(referer, status_code=303)
